package com.unknowpupil.web.component.wallet.service.impl;


import com.unknowpupil.web.component.annotation.dto.EntityTool;
import com.unknowpupil.web.component.wallet.entity.CoinWallet;
import com.unknowpupil.web.component.wallet.factory.Web3jFactory;
import com.unknowpupil.web.component.wallet.service.CoinToolsService;
import com.unknowpupil.web.component.wallet.tools.BigDecimalTools;
import com.unknowpupil.web.domain.Transaction;
import com.unknowpupil.web.exception.MxException;
import com.unknowpupil.web.service.IAccountInfoService;
import com.unknowpupil.web.service.ITransactionService;
import com.unknowpupil.web.service.IWalletService;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.bitcoinj.crypto.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.web3j.crypto.*;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameter;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.methods.response.*;
import org.web3j.tx.Transfer;
import org.web3j.utils.Convert;
import org.web3j.utils.Numeric;

import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.List;
import java.util.concurrent.ExecutionException;
import com.unknowpupil.web.domain.Wallet;

/**
 * @Classname: EthTools
 * @Author: <a> w10g </a>
 * @CreateDate: Create in 2019/10/18 14:38
 * @Description: {"privateKey":"3a3e6f39648ade267457e3a6a788334b17546c008562d561831282e3a91f2eda",
 * "address":"0x4a7cb23d273b57337fadc3b570b150cb7cd3b6c8",
 * "publicKey":"1009497f3790a897668e36cc2d8bcaf201003fc4f29fc93e6d56c06f10ded0db7a389be4d4c35afabc5124bccb26603814a636f21b0c79b3f4599b9eb57f11f8"}
 * @Version: 1.0
 */
@Data
@Service
public class EthCoinToolsImpl implements CoinToolsService {


    private final File eth_file = new File("c:/account/eth/");
    final static Logger logger = LoggerFactory.getLogger(CoinToolsService.class);
    /**
     * 中心账户
     */
    @Value("${eth.config.eth_center_address}")
    private String ethCenterAddress;

    /**
     * gasPrice和gasLimit影响的是转账的速度，如果gas过低，
     * 矿工会最后才打包你的交易。
     * 在app中，通常给定一个默认值，并且允许用户自己选择手续费。
     * <p>
     * 如果不需要自定义的话，还有一种方式来获取。
     * 获取以太坊网络最新一笔交易的gasPrice,
     * 转账的话，gasLimit一般设置为21000就可以了
     * 手续费需要 gasPrice*gasLimit
     * gas_price 如果想转得块，就需要获取最新一笔交易得price值来求不过，手续费就会有点高了
     */
    @Value("${eth.config.gas_price}")
    private BigInteger gasPrice;

    @Value("${eth.config.gas_limit}")
    private BigInteger gasLimit;

    @Autowired
    private ITransactionService transactionService;
    @Autowired
    private IAccountInfoService accountInfoService;
   @Autowired
   private IWalletService walletService;
    private Web3j web3j;

    @PostConstruct
    @Override
    public void init() throws Exception {
        logger.info(">>>>>>>>>>>>>>>>>>>>>>>>>>ETH节点开启>>>>>>>>>>>>");
        web3j = Web3jFactory.build();
    }


    /**
     * 创建钱包
     * 如果是写做app的话最好创建一个文件目录，然后把这个生成的公司要存储进去。
     * @throws MnemonicException.MnemonicLengthException
     */
    /**
     * 功能描述: <br>
     * 〈创建钱包
     * 如果是写做app的话最好创建一个文件目录，然后把这个生成的公司要存储进去。〉
     *
     * @param password
     * @return:
     * @since: 1.0.0
     * @Author: sunRain
     * @Date: 2019/11/7 10:46
     */
    @Override
    public CoinWallet createWallet(String password) throws MxException {
        try {
            Bip39Wallet wallet = WalletUtils.generateBip39Wallet(password, getEth_file());
            /**
             * 这是生成一个新的文件公私钥可从这里获取
             *   String fileName = WalletUtils.generateNewWalletFile(password, getEth_file());
             */
            String keyStoreKeyFile = getEth_file().getPath();
            String memorizingWords = wallet.getMnemonic();
            String filename = wallet.getFilename();
            Credentials credentials = WalletUtils.loadBip39Credentials(password,
                    wallet.getMnemonic());
            String address = credentials.getAddress();
            String publicKey = credentials.getEcKeyPair().getPublicKey().toString(16);
            String privateKey = credentials.getEcKeyPair().getPrivateKey().toString(16);
            return EntityTool.createCoinWallet(filename, keyStoreKeyFile, memorizingWords, address, publicKey, privateKey);
        } catch (Exception e) {
            logger.error(e.getMessage());
            throw new MxException("创建ETH钱包出错");
        }


    }

    /**
     * 功能描述: <br>
     * 〈根据资源读取私钥公钥〉
     *
     * @param password
     * @param source
     * @return:
     * @since: 1.0.0
     * @Author: sunRain
     * @Date: 2019/11/7 10:45
     */
    @Override
    public CoinWallet readWalletBySource(String password, String source) throws MxException {
        try {
            Credentials credentials = WalletUtils.loadCredentials(password, getEth_file() + "/" + source);
            String address = credentials.getAddress();
            String publicKey = credentials.getEcKeyPair().getPublicKey().toString(16);
            String privateKey = credentials.getEcKeyPair().getPrivateKey().toString(16);
            return EntityTool.createCoinWallet(null, null, null, address, publicKey, privateKey);
        } catch (Exception e) {
            logger.error(e.getMessage());
            if (StringUtils.containsOnly(e.getMessage(), "Invalid password provided")) {
                throw new MxException("密码错误");
            }
            throw new MxException("资源名错误");
        }
    }


    /**
     * web3.eth.getBalance方法返回链上指定地址的账户余额
     *
     * @param address
     * @return
     */
    @Override
    public double getBalance(String address) throws Exception {
        try {
            /**
             * addressHexString：	String - 要查询余额的地址。
             * defaultBlock：	Number|String -（可选）如果不设置此值，将使用web3.eth.defaultBlock设定的块，否则使用指定的块。
             * callback：	Funciton - （可选）回调函数，用于支持异步的执行方式。
             * 返回值：             *
             * String - 一个包含给定地址的当前余额的BigNumber实例，单位为wei
             */
            BigInteger balanceWei = web3j.ethGetBalance(address, DefaultBlockParameterName.LATEST).send().getBalance();
            BigDecimal balanceEther = Convert.fromWei(balanceWei.toString(), Convert.Unit.ETHER);
            return balanceEther.doubleValue();
        } catch (Exception e) {
            logger.warn("获取余额出错{}", e.getMessage());
        }
        return 0.0;
    }

    /**
     * 获取区块高度
     *
     * @return
     */
    @Override
    public int getBlockHeight() throws Exception {
        return web3j.ethBlockNumber().send().getBlockNumber().intValue();
    }

    /**
     * 根据区块高度处理区块信息
     *
     * @param height
     * @return
     * @throws Exception
     */
    @Override
    public boolean parseBlock(long height) throws Exception {

        try {
            EthBlock ethBlock = web3j.ethGetBlockByNumber(DefaultBlockParameter.valueOf(BigInteger.valueOf(height)), true).send();
            EthBlock.Block block = ethBlock.getBlock();
            List<EthBlock.TransactionResult> transactionResults = block.getTransactions();
            logger.info("处理ETH区块，高度：{}", height);
            //遍历区块中所有的交易
            transactionResults.forEach(transactionResult->{
                EthBlock.TransactionObject object = (EthBlock.TransactionObject) transactionResult.get();
                parserTransactionInfo(object);
            });
            logger.info("高度处理结束->{}", height);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            logger.info("处理ETH区块信息失败，高度：{}", height);
        }
        return true;
    }


    /**
     * 处理交易信息  ： 扫块监听
     * 有乐观锁不需要同步锁，同步锁消耗性能比乐观锁大。
     * 交易增加后应该同步一次区块  ： 只能监听eth todo:代币未弄
     *  sender Address 发送方
     *  to contract address 被掉用的合约地址
     *  tx data 调用的函数，
     *  gas used 交易花了多少汽油费
     *  gas price 单个汽油费都是钱
     *  gas limit 这个交易我最大愿意花多少汽油
     *
     * @param object
     * @throws Exception
     */
    @Override
    public void parserTransactionInfo(Object object) {
        //首先获取接收方地址查看是否是本系统的。
        String to = ((EthBlock.TransactionObject) object).getTo();
        if(StringUtils.isBlank(to)){
            return;
        }

        //数据库里面不区分大小写
        int count = accountInfoService.findAddressByTo(to);
        if (count > 0) {
            // 交易哈希  应该存在数据库中，以防重复充币
            String tx = ((EthBlock.TransactionObject) object).getHash();
            if( transactionService.unique("txnHash", tx)){
                // todo: input 暂时不知
                String input = ((EthBlock.TransactionObject) object).getInput();
                // 发送的地址
                String send = ((EthBlock.TransactionObject) object).getFrom();
                //高度
                String blockHash = ((EthBlock.TransactionObject) object).getBlockHash();
                //  交易次数
                BigInteger nonce = ((EthBlock.TransactionObject) object).getNonce();
                //精确计算
                double amount = BigDecimalTools.div(((EthBlock.TransactionObject) object).getValue().doubleValue() + "", Math.pow(10, 18) + "");
                Transaction eth = transactionService.createTransaction(send, to, tx, Long.parseLong(blockHash), amount, "ETH", nonce);
                transactionService.save(eth);
                //本机地址充币 做相应记录
                Wallet wallet = walletService.getWalletByCurrencyAndAddress("ETH",to);
                double add = BigDecimalTools.add(wallet.getCurrency() + "", amount + "");
                wallet.setBalance(new BigDecimal(add));
                walletService.save(wallet);
            }

        }

    }


    /**
     * 可更加适应判断
     *
     * @throws IOException
     */
    public void allSendInfo() throws IOException {
        //查询所有最新的区块交交易  DefaultBlockParameterName BigInteger 类型
        List<EthBlock.TransactionResult> txs = web3j.ethGetBlockByNumber(DefaultBlockParameterName.LATEST, true).send().getBlock().getTransactions();
        //循环
        txs.forEach(tx -> {
            //得到发送对象信息
            EthBlock.TransactionObject transaction = (EthBlock.TransactionObject) tx.get();
            //得到发送发
            System.out.println(transaction.getFrom());
        });
    }

    /**
     * 发起一笔交易（自定义参数）
     *
     * @param from       发起人钱包地址
     * @param to         转入的钱包地址
     * @param value      转账金额，单位是wei
     * @param privateKey 钱包私钥
     * @param gasPrice   转账费用
     * @param gasLimit
     * @param data       备注的信息
     * @throws IOException
     * @throws CipherException
     * @throws ExecutionException
     * @throws InterruptedException
     */
    public EthSendTransaction transfer(String from,
                                       String to,
                                       BigInteger value,
                                       String privateKey,
                                       BigInteger gasPrice,
                                       BigInteger gasLimit,
                                       String data) throws IOException, CipherException, ExecutionException, InterruptedException {


        //加载转账所需的凭证，用私钥
        Credentials credentials = Credentials.create(privateKey);
        //获取nonce，交易笔数
        BigInteger nonce = getNonce(from);
        //创建RawTransaction交易对象
        RawTransaction rawTransaction = RawTransaction.createEtherTransaction(nonce, gasPrice, gasLimit, to, value);
        //签名Transaction，这里要对交易做签名
        byte[] signMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
        String hexValue = Numeric.toHexString(signMessage);
        //发送交易
        EthSendTransaction ethSendTransaction = web3j.ethSendRawTransaction(hexValue).sendAsync().get();
        return ethSendTransaction;
    }

    /**
     * 获取nonce，交易笔数
     * nonce是指发起交易的账户下的交易笔数，每一个账户nonce都是从0开始，当nonce为0的交易处理完之后，才会处理nonce为1的交易，并依次加1的交易才会被处理
     *
     * @param from
     * @return
     * @throws ExecutionException
     * @throws InterruptedException
     */
    private BigInteger getNonce(String from) throws ExecutionException, InterruptedException {
        EthGetTransactionCount transactionCount = web3j.ethGetTransactionCount(from, DefaultBlockParameterName.LATEST).sendAsync().get();
        BigInteger nonce = transactionCount.getTransactionCount();
        logger.info("transfer nonce : {}", nonce);
        return nonce;
    }

    /**
     * 获取当前以太坊网络中最近一笔交易的gasPrice
     */
    public BigInteger requestCurrentGasPrice() throws Exception {
        EthGasPrice ethGasPrice = web3j.ethGasPrice().sendAsync().get();
        return ethGasPrice.getGasPrice();
    }


    /**
     * 发起一笔交易
     * 使用以太钱包文件发送以太币给其他人，不能设置nonce：（推荐）
     *
     * @param privateKey
     */
    public String transfer(String toAddress, BigDecimal value, String privateKey) throws Exception {
        //转账者私钥
        Credentials credentials = Credentials.create(privateKey);
        TransactionReceipt transactionReceipt = Transfer.sendFunds(
                web3j, credentials, toAddress,
                value, Convert.Unit.ETHER).sendAsync().get();
        String transactionHash = transactionReceipt.getTransactionHash();
        logger.info("transfer : {}", transactionHash);
        return transactionHash;
    }
//    /**
//     * 开启轮询
//     * 根据txhash查询交易是否被打包进区块
//     *
//     * @param txHash
//     */
//    public static void startPolling(String txHash) {
//        //5s刷新一次
//        disposable = Flowable.interval(0, 5, TimeUnit.SECONDS)
//                .compose(ScheduleCompat.apply())
////                .takeUntil(Flowable.timer(120, TimeUnit.SECONDS))
//                .subscribe(new Consumer<Long>() {
//                    @Override
//                    public void accept(Long num) throws Exception {
//                        Log.i(TAG, "第 : " + num + " 次轮询");
//                        //根据blockHash来判断交易是否被打包
//                        String blockHash = getBlockHash(txHash);
//                        boolean isSuccess = Numeric.toBigInt(blockHash).compareTo(BigInteger.valueOf(0)) != 0;
//                        if (isSuccess) {
//                            getTransactionReceipt(txHash);
//                        }
//                    }
//                });
//    }
//    /**
//     * 停止轮询
//     *
//     * @param disposable
//     */
//    public static void stopPolling(Disposable disposable) {
//        if (!disposable.isDisposed()) {
//            disposable.dispose();
//        }
//    }
//
//    /**
//     * 获取blockhash
//     * @param txHash
//     * @return
//     */
//    public static String getBlockHash(String txHash) {
//        Web3j web3j = Web3j.build(new HttpService("https://rinkeby.infura.io/v3/xxxx"));
//        try {
//            EthTransaction transaction = web3j.ethGetTransactionByHash(txHash).sendAsync().get();
//            Transaction result = transaction.getResult();
//            String blockHash = result.getBlockHash();
//            logger.info("getTransactionResult blockHash : {}", blockHash);
//            boolean isSuccess = Numeric.toBigInt(blockHash).compareTo(BigInteger.valueOf(0)) != 0;
//            if (isSuccess) {
//                getTransactionReceipt(txHash);
//                stopPolling(disposable);
//            }
//            return blockHash;
//        } catch (Exception e) {
//            e.printStackTrace();
//            return "";
//        }
//    }
//    public static void getTransactionReceipt(String transactionHash) {
//        Web3j web3j = Web3j.build(new HttpService("https://rinkeby.infura.io/v3/xxxx"));
//        try {
////            EthGetTransactionReceipt transactionReceipt = web3j.ethGetTransactionReceipt(transactionHash).sendAsync().get();
//
//            EthGetTransactionReceipt transactionReceipt = web3j.ethGetTransactionReceipt(transactionHash).sendAsync().get();
////            Optional<TransactionReceipt> transactionReceipt1 = transactionReceipt.getTransactionReceipt();
//            String status = receipt.getStatus();
//            BigInteger gasUsed = receipt.getGasUsed();
//            BigInteger blockNumber = receipt.getBlockNumber();
//            String blockHash = receipt.getBlockHash();
//            Log.i(TAG, "getTransactionReceipt status : " + status);
//            Log.i(TAG, "getTransactionReceipt gasUsed : " + gasUsed);
//            Log.i(TAG, "getTransactionReceipt blockNumber : " + blockNumber);
//            Log.i(TAG, "getTransactionReceipt blockHash : " + blockHash);
//
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        } catch (ExecutionException e) {
//            e.printStackTrace();
//        }
//    }

    /**
     * 离线签名eth
     *
     * @param to//转账的钱包地址
     * @param nonce//获取到的交易次数
     * @param gasPrice
     * @param gasLimit
     * @param value           //转账的值
     * @return
     */
    public static String signedEthTransactionData(String privateKey, String to, BigInteger nonce, BigInteger gasPrice, BigInteger gasLimit, String value) throws Exception {
//        if (words == null || words.size() != 12) {
//            throw new RuntimeException("please generateMnemonic first");
//        }
        //把十进制的转换成ETH的Wei, 1ETH = 10^18 Wei
        BigDecimal realValue = Convert.toWei(value, Convert.Unit.ETHER);
        RawTransaction rawTransaction = RawTransaction.createEtherTransaction(nonce, gasPrice, gasLimit, to, realValue.toBigIntegerExact());
        //手续费= (gasPrice * gasLimit ) / 10^18 ether

        Credentials credentials = Credentials.create(privateKey);
        //使用TransactionEncoder对RawTransaction进行签名操作
        byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
        //        //转换成0x开头的字符串
        return Numeric.toHexString(signedMessage);
    }

    /**
     * 离线签名eth
     *
     * @param contractAddress//合约地址
     * @param to//转账的钱包地址
     * @param nonce//获取到的交易次数
     * @param gasPrice
     * @param gasLimit
     * @param value                 //转账的值
     * @return
     */
    public static String signedEthContractTransactionData(String privateKey, String contractAddress, String to, BigInteger nonce, BigInteger gasPrice, BigInteger gasLimit, Double value, Double decimal) throws Exception {
//        if (words == null || words.size() != 12) {
//            throw new RuntimeException("please generateMnemonic first");
//        }
        //因为每个代币可以规定自己的小数位, 所以实际的转账值=数值 * 10^小数位
        BigDecimal realValue = BigDecimal.valueOf(value * Math.pow(10.0, decimal));

        //0xa9059cbb代表某个代币的转账方法hex(transfer) + 对方的转账地址hex + 转账的值的hex
        String data = "0xa9059cbb" + Numeric.toHexStringNoPrefixZeroPadded(Numeric.toBigInt(to), 64) + Numeric.toHexStringNoPrefixZeroPadded(realValue.toBigInteger(), 64);
        RawTransaction rawTransaction = RawTransaction.createTransaction(nonce, gasPrice, gasLimit, contractAddress, data);
        //手续费= (gasPrice * gasLimit ) / 10^18 ether

        Credentials credentials = Credentials.create(privateKey);
        //使用TransactionEncoder对RawTransaction进行签名操作
        byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
        //转换成0x开头的字符串
        return Numeric.toHexString(signedMessage);
    }
}
